Data Mining Versuch Analyse Globaler Gesundheitsdaten

  • Autor: Prof. Dr. Johannes Maucher
  • Datum: 18.10.2018

Abzugeben ist das Jupyter Notebook mit dem verlangten Implementierungen, den entsprechenden Ausgaben, Antworten und Diskussionen/Beschreibungen. Das Notebook ist als .ipynb und als .html abzugeben.

Einführung

Lernziele:

In diesem Versuch sollen Kenntnisse in folgenden Themen vermittelt werden:

  • Datenimport und Datenexport von und zu
    • Pandas Dataframes
    • PostgreSQL Datenbanken
  • Explorative Datenanalysen (EDA)
  • Interaktive Datenvisualisierung mit Bokeh
  • Dimensionsreduktion mit PCA und TSNE
  • Überwachtes Lernen eines Regressionsmodells
  • Unüberwachtes Lernen mit K-Means
  • Zeitreihen-Visualisierung

Vorbereitung

Datenbankzugriff

  1. Installieren Sie PostgreSQL. Mit PostgreSQL sollte auch pgAdmin installiert werden. PgAdmin ist eine open-source Software für die Entwicklung und die Administration von PostgreSQL Datenbanken.
  2. Legen Sie über pgAdmin eine Datenbank für das Datamining-Praktikum an. In diese Datenbank werden alle in diesem Versuch relevanten Tabellen geschrieben.
  3. Für den Datenbankzugriff aus Python heraus wird in diesem Versuch SQLAlchemy eingesetzt. Machen Sie sich mit den Basics von SQLAlchemy vertraut, z.B. mithilfe von https://gitlab.mi.hdm-stuttgart.de/maucher/DataScienceProgramming/blob/master/Python/Lecture/07DataBasePandas.ipynb, Abschnitt Using SQLAlchemy and Pandas.

Pandas Dataframe

Machen Sie sich mit den Grundlagen von Pandas vertraut.

Visualisierung mit Bokeh

Machen Sie sich mit den Grundlagen von Bokeh vertraut.

Dimensionsreduktion

Machen Sie sich mit der Hauptachsentransformation (PCA) und dem t-SNE Verfahren vertraut.

Regression

Machen Sie sich mit Linearer Regression und Random Forest Regression vertraut.

Clustering

Machen Sie sich mit dem k-means Clustering Algorithmus vertraut.

Durchführung

Einlesen der Daten aus .csv und Ablage in PostgreSQL

In diesem ersten Teil des Versuchs müssen alle relevanten Daten aus .csv-Files eingelesen und in PostgreSQL-Tabellen abgelegt werden. Alle benötigten .csv Files befinden sich im Verzeichnis gesundheitsdaten. Die Daten stammen aus folgenden Quellen:

Allgemeine Daten

Das unten gegebene Dictionary filenames_general definiert aus welchen Dateien (keys), welche Spalten (values) gebraucht werden. Die Bezeichnung der Spalten in den Files weicht von der Spaltenbezeichnung im Dictionary ab. Die im Dictionary angegebenen Namen sollen aber in diesem Versuch (in den Pandas Dataframes und in den Datenbank-Tabellen) verwendet werden.

In [271]:
filenames_general = {'life-expectancy.csv':['Entity','Code','Year','LifeExpectancy'],
    'gdp-per-capita-worldbank.csv':['Entity','Code','Year','GDPperCapita'],
    'annual-healthcare-expenditure-per-capita.csv':['Entity','Code','Year','AnnualHealthcarExpPerCapita'],
    'annual-working-hours-per-persons-engaged.csv':['Entity','Code','Year','AnnualWorkingHourPerPerson']
}
  1. Laden Sie die relevanten Daten dieser Files in einen Pandas Dataframe. Die Spalten des resultierenden Dataframes sind Entity, Code, Year, LifeExpectancy, GDPperCapita, AnnualHealthcarExpPerCapita, AnnualWorkingHourPerPerson. Als Keys sind Code, Entity und Year zu verwenden. Zeilen in denen mindestens einer dieser Keys fehlt, sind im Dataframe und in der Datenbank zu ignorieren.

Tipp: Verwenden Sie für das Zusammenführen der Daten aus verschiedenen Files die Methode merge() des Pandas-Dataframes.

  1. Zeigen Sie für den angelegten Dataframe
    • die ersten 10 Zeilen
    • die Größe (Anzahl Zeilen und Anzahl Spalten)
    • die Anzahl der NaNs pro Spalte

an.

  1. Schreiben Sie den angelegten Dataframe mit der Pandas Methode to_sql() in eine Datenbanktabelle mit dem Namen general_information.
In [272]:
import psycopg2 #provides drivers for PostgreSQL
import numpy as np
np.set_printoptions(precision=2,suppress=True)
import json #required to access json file
import pandas as pd
import geopandas as gpd

import pprint
pp = pprint.PrettyPrinter(indent=4)
In [273]:
# The code reads the contents of the .json file into a Python dictionary.
with open('../db/configLocalP1.json') as f:
    conf = json.load(f)
In [274]:
# 1. Aufgabe

# Parse data from the provided csv files via Pandas library
result = pd.DataFrame
result2 = pd.DataFrame
general_information = pd.DataFrame
for key in filenames_general:
  csv_file = '../data/gesundheitsdaten_{}'.format(key)
  general_information = pd.read_csv(csv_file, names=filenames_general[key], header=0)
  general_information = general_information.dropna(subset=['Code', 'Year']) # Drop missing values.
   
  if result.empty:
     result = general_information
  else:
    result = result.merge(general_information, how='left', on=['Entity' ,'Code', 'Year'])
    
#print('Merged result1:{}'.format(result.head(10)))
In [275]:
# 2. Aufgabe

# Print 10 values to check 
print(general_information.head(10))
# Number of rows
print('Total number of rows: {}'.format(general_information.shape[0]))
# Number of columns
print('Total number of columns: {}'.format(general_information.shape[1]))
# Investigating missing values 
print(general_information.isnull().sum(axis = 0))
Entity Code  Year  AnnualWorkingHourPerPerson
0  Argentina  ARG  1950                 2034.000000
1  Argentina  ARG  1951                 2037.866753
2  Argentina  ARG  1952                 2041.740856
3  Argentina  ARG  1953                 2045.622325
4  Argentina  ARG  1954                 2049.511172
5  Argentina  ARG  1955                 2053.407412
6  Argentina  ARG  1956                 2057.311059
7  Argentina  ARG  1957                 2061.222128
8  Argentina  ARG  1958                 2065.140631
9  Argentina  ARG  1959                 2069.066584
Total number of rows: 3319
Total number of columns: 4
Entity                        0
Code                          0
Year                          0
AnnualWorkingHourPerPerson    0
dtype: int64
In [276]:
# Create connection string to connect to PostgreSQL database
from sqlalchemy import create_engine
conn_str ='postgresql://{}:{}@localhost:{}/{}'.format(conf['user'], conf['passw'], conf['port'], conf['database'])
engine = create_engine(conn_str)
In [277]:
# 3. Aufgabe

table_name = 'general_information'
# Check if datatable already exists,then try to
# Write records stored in a dataFrame to SQL database.
if not engine.has_table(table_name):
    try:
        result.to_sql(name=table_name, index=False, con=engine)
    except (MySQLdb.Error, MySQLdb.Warning) as e:
        print('Error {} occured writing to database.'.format(e))
else:
    print('The table {} already exists.'.format(table_name))

general_information = result.copy()
The table general_information already exists.

Ernährungsdaten

Lesen Sie nun gleich wie oben die Ernährungsdaten ein. Die relevanten Spalten der entsprechenden Files sind nun im Dictionary filenames_nutrition definiert. Diese Daten sind in einer Datenbanktabelle nutrition_information anzulegen.

In [278]:
filenames_nutrition = {
    'life-expectancy.csv':['Entity','Code','Year','LifeExpectancy'],
    'fruit-consumption-per-capita-kilograms-per-year.csv':
            ['Entity','Code','Year','AnnualFruitConsumptionPerCapita'],
    'vegetable-consumption-per-capita-kilograms-per-year.csv':
            ['Entity','Code','Year','AnnualVegetableConsumptionPerCapita'],
    'dietary-compositions-by-commodity-group-1961-2013.csv':
            ['Entity','Code','Year','KcalOther','KcalSugar',
            'KcalOilsFats','KcalMeat','KcalDairyEggs',
            'KcalFruitsVegetables','KcalStarchyRoots','KcalPulses',
            'KcalCerealsGrains','KcalAlcoholicBeverages'],
    'daily-caloric-supply-derived-from-carbohydrates-protein-and-fat.csv':
            ['Entity','Code','Year','KcalAnimalProtein',
            'KcalPlantProtein','KcalFat','KcalCarbohydrates'],
    'daily-per-capita-supply-of-calories.csv':['Entity','Code','Year','DailyCaloriesPerCapita']
}
In [279]:
# Parse data from the provided csv files via Pandas library
result = pd.DataFrame
nutrition_information = pd.DataFrame
for key in filenames_nutrition:
  csv_file = '../data/gesundheitsdaten_{}'.format(key)
  nutrition_information = pd.read_csv(csv_file, names=filenames_nutrition[key], header=0)
  nutrition_information = nutrition_information.dropna(subset=['Code', 'Year']) # Drop missing values.

  # Plot the first rows, columns of the Pandas data frame
  print('First lines in dataframe:\n{}'.format(nutrition_information.head()))
  # Investigating over missing values
  print('Information about dataframe:\n{}'.format(nutrition_information.info()))
     
  if result.empty:
    result = nutrition_information
  else:
    result = result.merge(nutrition_information, how='left', on=['Entity' ,'Code', 'Year'])
First lines in dataframe:
        Entity Code  Year  LifeExpectancy
0  Afghanistan  AFG  1950       27.537001
1  Afghanistan  AFG  1951       27.809999
2  Afghanistan  AFG  1952       28.350000
3  Afghanistan  AFG  1953       28.879999
4  Afghanistan  AFG  1954       29.399000
<class 'pandas.core.frame.DataFrame'>
Int64Index: 15386 entries, 0 to 17893
Data columns (total 4 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   Entity          15386 non-null  object 
 1   Code            15386 non-null  object 
 2   Year            15386 non-null  int64  
 3   LifeExpectancy  15386 non-null  float64
dtypes: float64(1), int64(1), object(2)
memory usage: 601.0+ KB
Information about dataframe:
None
First lines in dataframe:
        Entity Code  Year  AnnualFruitConsumptionPerCapita
0  Afghanistan  AFG  1961                            41.13
1  Afghanistan  AFG  1962                            38.00
2  Afghanistan  AFG  1963                            38.25
3  Afghanistan  AFG  1964                            47.92
4  Afghanistan  AFG  1965                            48.69
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8260 entries, 0 to 10392
Data columns (total 4 columns):
 #   Column                           Non-Null Count  Dtype  
---  ------                           --------------  -----  
 0   Entity                           8260 non-null   object 
 1   Code                             8260 non-null   object 
 2   Year                             8260 non-null   int64  
 3   AnnualFruitConsumptionPerCapita  8260 non-null   float64
dtypes: float64(1), int64(1), object(2)
memory usage: 322.7+ KB
Information about dataframe:
None
First lines in dataframe:
        Entity Code  Year  AnnualVegetableConsumptionPerCapita
0  Afghanistan  AFG  1961                                36.75
1  Afghanistan  AFG  1962                                37.47
2  Afghanistan  AFG  1963                                38.87
3  Afghanistan  AFG  1964                                40.17
4  Afghanistan  AFG  1965                                40.83
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8260 entries, 0 to 10392
Data columns (total 4 columns):
 #   Column                               Non-Null Count  Dtype  
---  ------                               --------------  -----  
 0   Entity                               8260 non-null   object 
 1   Code                                 8260 non-null   object 
 2   Year                                 8260 non-null   int64  
 3   AnnualVegetableConsumptionPerCapita  8260 non-null   float64
dtypes: float64(1), int64(1), object(2)
memory usage: 322.7+ KB
Information about dataframe:
None
First lines in dataframe:
        Entity Code  Year  KcalOther  KcalSugar  KcalOilsFats  KcalMeat  \
0  Afghanistan  AFG  1961         13         51            92        88   
1  Afghanistan  AFG  1962         12         45            98        88   
2  Afghanistan  AFG  1963         13         47           106        91   
3  Afghanistan  AFG  1964         11         55           102        93   
4  Afghanistan  AFG  1965         13         57           105        95   

   KcalDairyEggs  KcalFruitsVegetables  KcalStarchyRoots  KcalPulses  \
0            102                    82                25          16   
1            101                    76                22          17   
2            110                    79                23          17   
3            110                    95                24          18   
4            118                    95                24          18   

   KcalCerealsGrains  KcalAlcoholicBeverages  
0               2530                     0.0  
1               2458                     0.0  
2               2212                     0.0  
3               2445                     0.0  
4               2431                     0.0  
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8101 entries, 0 to 8153
Data columns (total 13 columns):
 #   Column                  Non-Null Count  Dtype  
---  ------                  --------------  -----  
 0   Entity                  8101 non-null   object 
 1   Code                    8101 non-null   object 
 2   Year                    8101 non-null   int64  
 3   KcalOther               8101 non-null   int64  
 4   KcalSugar               8101 non-null   int64  
 5   KcalOilsFats            8101 non-null   int64  
 6   KcalMeat                8101 non-null   int64  
 7   KcalDairyEggs           8101 non-null   int64  
 8   KcalFruitsVegetables    8101 non-null   int64  
 9   KcalStarchyRoots        8101 non-null   int64  
 10  KcalPulses              8101 non-null   int64  
 11  KcalCerealsGrains       8101 non-null   int64  
 12  KcalAlcoholicBeverages  8048 non-null   float64
dtypes: float64(1), int64(10), object(2)
memory usage: 886.0+ KB
Information about dataframe:
None
First lines in dataframe:
        Entity Code  Year  KcalAnimalProtein  KcalPlantProtein  KcalFat  \
0  Afghanistan  AFG  1961              54.12            285.52   337.59   
1  Afghanistan  AFG  1962              53.92            278.00   338.49   
2  Afghanistan  AFG  1963              56.80            251.68   347.13   
3  Afghanistan  AFG  1964              57.32            276.64   350.55   
4  Afghanistan  AFG  1965              59.76            275.68   357.57   

   KcalCarbohydrates  
0            2321.77  
1            2246.59  
2            2042.39  
3            2268.49  
4            2262.99  
<class 'pandas.core.frame.DataFrame'>
Int64Index: 8877 entries, 0 to 8980
Data columns (total 7 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   Entity             8877 non-null   object 
 1   Code               8877 non-null   object 
 2   Year               8877 non-null   int64  
 3   KcalAnimalProtein  8877 non-null   float64
 4   KcalPlantProtein   8877 non-null   float64
 5   KcalFat            8877 non-null   float64
 6   KcalCarbohydrates  8877 non-null   float64
dtypes: float64(4), int64(1), object(2)
memory usage: 554.8+ KB
Information about dataframe:
None
First lines in dataframe:
        Entity Code  Year  DailyCaloriesPerCapita
0  Afghanistan  AFG  1961                  2999.0
1  Afghanistan  AFG  1962                  2917.0
2  Afghanistan  AFG  1963                  2698.0
3  Afghanistan  AFG  1964                  2953.0
4  Afghanistan  AFG  1965                  2956.0
<class 'pandas.core.frame.DataFrame'>
Int64Index: 9119 entries, 0 to 9550
Data columns (total 4 columns):
 #   Column                  Non-Null Count  Dtype  
---  ------                  --------------  -----  
 0   Entity                  9119 non-null   object 
 1   Code                    9119 non-null   object 
 2   Year                    9119 non-null   int64  
 3   DailyCaloriesPerCapita  9119 non-null   float64
dtypes: float64(1), int64(1), object(2)
memory usage: 356.2+ KB
Information about dataframe:
None
In [280]:
table_name = 'nutrition_information'
# Check if datatable already exists,then try to
# Write records stored in a DataFrame to SQL database.
if not engine.has_table(table_name):
    try:
        result.to_sql(name=table_name, index=False, con=engine)
    except (MySQLdb.Error, MySQLdb.Warning) as e:
        print('Error {} occured writing to database.'.format(e))
else:
    print('The table {} already exists.'.format(table_name))
    
nutrition_information = result.copy()
The table nutrition_information already exists.

Exemplarische Datenbankabfragen

  1. Lesen Sie die Datenbanktabelle general_information in einen Pandas Dataframe.
  2. Lesen Sie die Spalten Yearund AnnualWorkingHourPerPerson für alle zu Germany gehörende Zeilen aus der Tabelle general_information in einen Pandas Dataframe.

Anmerkung: Für diesen Versuch kommen relativ kleine Datenmengen zum Einsatz. In diesem Fall kann direkt auf den Pandas Dataframes gearbeitet werden. Für sehr große Datenmengen bietet es sich an, nicht alle Daten in den Arbeitsspeicher zu laden, sondern nur die aktuell benötigten durch entsprechende Datenbankabfragen.

In [281]:
# 1. Aufgabe
giTable = pd.read_sql("general_information", engine)
display(giTable.head(5))
Entity Code Year LifeExpectancy GDPperCapita AnnualHealthcarExpPerCapita AnnualWorkingHourPerPerson
0 Afghanistan AFG 1950 27.537001 NaN NaN NaN
1 Afghanistan AFG 1951 27.809999 NaN NaN NaN
2 Afghanistan AFG 1952 28.350000 NaN NaN NaN
3 Afghanistan AFG 1953 28.879999 NaN NaN NaN
4 Afghanistan AFG 1954 29.399000 NaN NaN NaN
In [282]:
# 2. Aufgabe
sqlQuery = """SELECT "Year", "AnnualWorkingHourPerPerson" FROM general_information WHERE "Entity" = 'Germany'"""
giTable2 = pd.read_sql(sqlQuery ,engine)
display(giTable2.head())
Year AnnualWorkingHourPerPerson
0 1875 NaN
1 1885 NaN
2 1895 NaN
3 1905 NaN
4 1911 NaN

Datenanalyse

Über die Zeit kann man erkennen, dass die Arbeitsstunden pro Jahr in Deutschland abnehmen. Dies könnte im Zusammenhang mit der zunehmenden Automatisierung/Industrialisierung von Arbeitstätigkeiten stehen.

Geographische Visualisierung der Lebenserwartung

In diesem Teilversuch soll die Lebenserwartung pro Land in einer Weltkarte visualisiert werden. Zu erstellen ist ein Plot der unten dargestellten Art:

mapLifeExpext

Laden der länderspezifischen Polygondaten

  1. Die Lebenserwartung pro Land soll auf einer Karte abgebildet werden. Hierfür werden zunächst die Länderumrisse benötigt, um diese plotten zu können. Die Daten dazu findet man z.B. hier: https://rawgit.com/johan/world.geo.json/master/countries.geo.json. Geodaten für die Länderumrisse können mithilfe der Python Library geopandas wie folgt eingelesen werden.
In [283]:
# Path to the geographical data stored in a .json file
PATH = '../data/json/gesundheitsdaten_'
# Load data into GeoDataFrame
countries = gpd.read_file(PATH + 'countries_geo.json')
In [284]:
# Examine country GeoDataFrame loaded from .json file 
countries.head()
Out[284]:
id name geometry
0 AFG Afghanistan POLYGON ((61.21082 35.65007, 62.23065 35.27066...
1 AGO Angola MULTIPOLYGON (((16.32653 -5.87747, 16.57318 -6...
2 ALB Albania POLYGON ((20.59025 41.85540, 20.46317 41.51509...
3 ARE United Arab Emirates POLYGON ((51.57952 24.24550, 51.75744 24.29407...
4 ARG Argentina MULTIPOLYGON (((-65.50000 -55.20000, -66.45000...
In [285]:
import descartes

# Test plot the countries with GeoPandas Descartes library
countries.plot()
Out[285]:
<matplotlib.axes._subplots.AxesSubplot at 0x25748790388>
  1. Da in diesem Teilversuch nur die Lebenserwartung in 2015 betrachtet werden, sind nur diese Zeilen aus dem angelegten Dataframe zu berücksichtigen.
  2. Vereinigen Sie die Polygondaten aus dem Dataframe countries mit dem Dataframe general_information. Achten Sie hierbei, dass Sie den pandas dataframe in den geopandas dataframe mergen, das Resultat also ein geopandas dataframe mergen ist (andernfalls funktioniert der nächste Schritt nicht).
In [286]:
# 1. Aufgabe

general_information_2015 = giTable.loc[giTable["Year"] == 2015][["Code", "LifeExpectancy"]]
In [287]:
# 2. Aufgabe

# Adjust names for the values of "Entity" and "Code" to "name" and "id"
general_information_2015.rename(columns={"Code":"id"}, inplace=True)
# Check the new values
print ('The new name-values are \n{}'.format(general_information_2015))
The new name-values are 
             id  LifeExpectancy
65          AFG       63.287998
131         ALB       78.174004
200         DZA       75.860001
267         AGO       61.241001
333         ATG       76.207001
...         ...             ...
15121       ESH       69.212997
15188  OWID_WRL       71.429001
15254       YEM       64.742996
15319       ZMB       61.396999
15385       ZWE       60.397999

[203 rows x 2 columns]
In [289]:
# Merge the two dataframes into a GeoDataFrame
countriesMerged = countries.merge(general_information_2015, how='left', on=["id"])
#print('The merged GeoDataFrame values are \n{}'.format(countriesMerged))
  1. Durch Aufruf der Methode to_json() kann ein Dataframe in json transformiert werden. Die von der Methode zurückgegebenen Daten können einem Bokeh GeoJSONDataSource-Objekt beim Anlegen übergeben werden. Das GeoJSONDataSource-Objekt kann direkt der entsprechenden Bokeh-plotting Funktion übergeben werden. Erzeugen Sie mit diesen Hinweisen einen interaktiven Weltkartenplot der oben dargestellten Art. Beim Mouse-Over über ein Land soll der Name des Landes und die Lebenserwartung angezeigt werden.

    Alternative: Eine einfachere Lösung für die Geovisualisierung bietet geopandas. Allerdings sind damit keine interaktiven Elemente realisierbar.

  2. Diskutieren Sie die aus der Visualisierung gewonnenen Erkenntnisse.
In [290]:
# Write the GeoPandaFrame to GeoJSON file format
countriesMerged.to_file("../data/json/lifeexp_geodata_2015.json", driver="GeoJSON")
In [291]:
# Import all required modules to render the interactive map in Bokeh
from bokeh.io import output_notebook, show
from bokeh.models import GeoJSONDataSource, LinearColorMapper, ColorBar
from bokeh.plotting import figure
from bokeh.palettes import Viridis256
import json

Bokeh Library for interactive maps

The merged files result in a GeoDataframe object, which can be represented by using a geopandas module. For in interactive visualization Bokeh library is used. Bokeh consumes GeoJSON format, which represents geographical features with JSON. GeoJSON describes points, lines and polygones (Patches) as a collection of features. Therefore the merged files are converted to a GeoJSON format.

In [292]:
# 3. Aufgabe

#GeoJSONDataSource Objekt erstellen
data = json.loads(countriesMerged.to_json())
# Convert the GeoJson data to a string-like object
geo_source = GeoJSONDataSource(geojson=json.dumps(data))
In [293]:
#Bokeh ausgabe im Notebook
output_notebook()

#Tooltip für Mouse-Over
TOOLTIPS = [
    ('Land', '@name'),
    ('Lebenserwartung', '@LifeExpectancy')
]

#Color Mapper für farbliche Visualisierung erstellen
Viridis256 = tuple(reversed(Viridis256))
color_mapper = LinearColorMapper(palette=Viridis256, low=countriesMerged.LifeExpectancy.min(), high=countriesMerged.LifeExpectancy.max())
color_mapper.low_color = "grey"

#Color bar als Legende
color_bar = ColorBar(color_mapper=color_mapper, 
                     label_standoff=12, border_line_color=None, location=(0,0))



#bokeh figure anlegen, mit zuvor erstelltem tooltip
p = figure (background_fill_color="lightgrey", plot_height = 600, plot_width= 1200, tooltips=TOOLTIPS)

#Länder hinzufügen mit farblich codierter Lebenserwartung
p.patches('xs', 'ys', source=geo_source, line_color="black", line_width=0.25, fill_alpha=1, fill_color = {'field' :'LifeExpectancy', 'transform' : color_mapper})

#Color bar der bokeh figure hinzufügen
p.add_layout(color_bar, 'right')
show(p)
Loading BokehJS ...

4. Aufgabe

Ergebnis der Visualisierung

Auf der mittels Bokeh visualisierten Weltkarte sind die Lebenserwartungen je Land dargestellt. Hierbei werden die einzelnen Länder mit einer Farbskala von dunkel-lila bis hell-gelb eingefärbt. Je dunkler die Farbgebung, desto höher die Lebenserwartung.

Bei der Analyse der Weltkarte ist besonders auffällig, dass sowohl für Serbien, Somalia, Grönland, als auch Antarktika (sowie kleinere Inseln) keine Daten in der CSV Datei vorhanden sind. Diese werden hier grau dargestellt. Dagegen ist die Lebenserwartung in industriell weiter entwickelten Nationen, wie Japan (~ ca. 83 Jahre), Australien (~ ca. 82.7 Jahre), Kanada (~ ca. 82.5 Jahre), Neuseeland (~ ca. 81 Jahre), als auch Zentral-Europa am höchsten. Die niedrigste Lebenserwartungen sind vor allem in Zentral-Afrika (~ ca. 50 - 60 Jahre), Indien (~ ca. 68 Jahre), sowie Russland (~ ca. 70.9 Jahre), als auch dem Süden Asiens zu verzeichnen.

Der Weltkarte nicht entnehmbar sind dabei politische, als auch gesellschaftliche Entwicklungen. Dennoch kann man aus dem Allgemeinwissen heraus schließen, dass sich die Geschichte u.A. Kolonialisierung der Länder in der Vergangenheit stark auf die Lebenserwartung und den Wohlstand in der Bevölkerung ausgewirkt hat.

Korrelation der Lebenserwartung mit volkswirtschaftlichen Metriken

In diesem Abschnitt soll die paarweise Korrelation von

  • GDPperCapita
  • AnnualHealthcarExpPerCapita

mit der LifeExpectancy untersucht werden.

  1. Für die visuelle Korrelationsanalyse erzeugen Sie je einen Scatterplot, in dem LifeExpectancy über GDPperCapita bzw. über AnnualHealthcarExpPerCapita dargestellt ist.
  2. Berechnen Sie mit der numpy-Funktion corrcoef die beiden paarweisen Korrelationen.
  3. Diskutieren Sie das Ergebnis.
In [294]:
import seaborn as sns
import matplotlib.pyplot as plt
In [295]:
# 1. Aufgabe

fig, ax = plt.subplots(1,2, figsize=(20,10))
sns.scatterplot(y="LifeExpectancy", x="GDPperCapita", data=giTable.dropna(), ax=ax[0])
sns.scatterplot(y="LifeExpectancy", x="AnnualHealthcarExpPerCapita", data=giTable.dropna(), ax=ax[1])

plt.show()
In [296]:
# 2. Aufgabe

corr = np.corrcoef(giTable[["LifeExpectancy","GDPperCapita","AnnualHealthcarExpPerCapita"]].dropna(), rowvar=False)
print("Korrelationen: \n"
     "GDPperCapital zu LifeExpectancy: " + str(corr[1][0]) + "\n"
     "AnnualHealthcarExpPerCapital zu LifeExpectancy: "  + str(corr[2][0]) + "\n")
Korrelationen: 
GDPperCapital zu LifeExpectancy: 0.5866407911282291
AnnualHealthcarExpPerCapital zu LifeExpectancy: 0.6193840129267437

Aufgabe 3:

Für beide Werte gilt: Für niedrige Werte haben die Merkmale geringen Einfluss auf die Lebenserwartung. Ab circa 10.000 GDPperCapita und ab circa 1.000 AnnualHealthcarExpPerCapita lässt sich eine starke Korrelation erkennen. Insgesamt haben wir eine moderate Korrelation da die Merkmale anfänglich kaum korrelieren.

Korrelation der Lebenserwartung mit Ernährungsdaten

Untersuchen Sie dann so wie im vorigen Abschnitt wie die einzelnen Nuitrition-Merkmale mit der LifeExpectancy korrelieren. Diskutieren Sie das Ergebnis.

In [297]:
nutritionTable = nutrition_information

#Bereich für mehrere Plots erstellen
fig, ax = plt.subplots(nrows=6, ncols=3, figsize=(25,30))

#plots hinzufügen
for i, col in enumerate(nutritionTable.columns[3:]):
    if col != "LifeExpectancy":
        sns.scatterplot(y="LifeExpectancy", x=col, data=nutritionTable.dropna(), ax=ax[(i-1)//3,(i-1)%3])
        
#leeren Plot entfernen
fig.delaxes(ax[5][2])
#Plot anzeigen
plt.show()

#Korrelationten zwischen den Merkmalen und Lebenserwartung
display(nutritionTable.corr()[["LifeExpectancy"]])
        
LifeExpectancy
Year 0.555273
LifeExpectancy 1.000000
AnnualFruitConsumptionPerCapita 0.300567
AnnualVegetableConsumptionPerCapita 0.486579
KcalOther 0.553525
KcalSugar 0.679232
KcalOilsFats 0.572730
KcalMeat 0.660039
KcalDairyEggs 0.679739
KcalFruitsVegetables 0.276923
KcalStarchyRoots -0.342655
KcalPulses -0.259308
KcalCerealsGrains -0.065449
KcalAlcoholicBeverages 0.470849
KcalAnimalProtein 0.725678
KcalPlantProtein 0.059454
KcalFat 0.725253
KcalCarbohydrates 0.353452
DailyCaloriesPerCapita 0.709219

Time Series Analysis

Auf der Basis des Dataframes nutrition_information: Stellen Sie die zeitliche Entwicklung des Merkmals

  1. KcalFat
  2. DailyCaloriesPerCapita

von 1995 bis 2015 für die Länder Bolivia, Bulgaria, Egypt, Kenya, United States, China, Brazil, Germany graphisch dar. Erzeugen Sie hierfür pro Merkmal einen Plot, in dem die Linegraphs aller genannten Länder dargestellt sind.

Interpretieren Sie diese Darstellungen

In [298]:
%matplotlib inline
from matplotlib import pyplot as plt
from matplotlib.pylab import rcParams
plt.style.use('ggplot')

# Additional Style elements
plt.rcParams['lines.linewidth'] = 3.5
plt.rcParams['lines.linestyle'] = '--'
plt.rcParams['lines.marker'] = 'o'
plt.rcParams['axes.titlepad'] = 25
plt.rcParams['axes.labelpad'] = 20
In [299]:
# Filter the dataframe
dfToPlot = nutrition_information.loc[(nutrition_information['Entity'].isin(['Bolivia', 'Bulgaria', 'Brazil', 'China', 'Egypt', 'Germany', 'Kenya', 'United States'])) & (nutrition_information['Year'] >= 1995)]
dfToPlot.head(5)
Out[299]:
Entity Code Year LifeExpectancy AnnualFruitConsumptionPerCapita AnnualVegetableConsumptionPerCapita KcalOther KcalSugar KcalOilsFats KcalMeat ... KcalFruitsVegetables KcalStarchyRoots KcalPulses KcalCerealsGrains KcalAlcoholicBeverages KcalAnimalProtein KcalPlantProtein KcalFat KcalCarbohydrates DailyCaloriesPerCapita
1542 Bolivia BOL 1995 57.862000 91.55 53.17 20.0 280.0 198.0 191.0 ... 180.0 138.0 19.0 961.0 40.0 86.64 134.60 399.42 1485.34 2106.0
1543 Bolivia BOL 1996 58.423000 91.12 55.15 21.0 278.0 227.0 197.0 ... 181.0 141.0 17.0 864.0 39.0 89.32 123.20 423.99 1410.49 2047.0
1544 Bolivia BOL 1997 58.987000 95.23 56.81 22.0 276.0 223.0 203.0 ... 187.0 134.0 22.0 869.0 38.0 90.24 125.76 424.44 1414.56 2055.0
1545 Bolivia BOL 1998 59.554001 93.64 53.01 19.0 272.0 176.0 216.0 ... 181.0 127.0 18.0 836.0 38.0 89.64 118.84 378.18 1364.34 1951.0
1546 Bolivia BOL 1999 60.122002 94.68 54.82 26.0 272.0 175.0 217.0 ... 182.0 152.0 22.0 881.0 42.0 89.80 131.04 381.06 1435.10 2037.0

5 rows × 21 columns

In [300]:
# List of countries for time series analysis
countries = dfToPlot['Entity'].unique()
In [301]:
# Data preprocessing to fill in NaN values with interpolation
result = pd.DataFrame()
# Iterate through each selection
for country in countries:
    result = nutrition_information.loc[nutrition_information['Entity'] == country]
    nutrition_information.loc[nutrition_information['Entity'] == country] = result.fillna(result.interpolate(method='linear', limit_area='inside'))

# Analyse the tail of the dataframe
nutrition_information.tail(5)
Out[301]:
Entity Code Year LifeExpectancy AnnualFruitConsumptionPerCapita AnnualVegetableConsumptionPerCapita KcalOther KcalSugar KcalOilsFats KcalMeat ... KcalFruitsVegetables KcalStarchyRoots KcalPulses KcalCerealsGrains KcalAlcoholicBeverages KcalAnimalProtein KcalPlantProtein KcalFat KcalCarbohydrates DailyCaloriesPerCapita
15381 Zimbabwe ZWE 2011 54.799999 17.19 16.26 18.0 232.0 343.0 98.0 ... 32.0 56.0 39.0 1257.0 66.0 51.40 159.20 537.66 1451.74 2200.0
15382 Zimbabwe ZWE 2012 56.515999 17.90 16.52 16.0 264.0 353.0 99.0 ... 31.0 55.0 35.0 1210.0 79.0 49.80 153.16 538.56 1455.48 2197.0
15383 Zimbabwe ZWE 2013 58.053001 15.32 16.04 14.0 268.0 340.0 91.0 ... 29.0 54.0 36.0 1147.0 76.0 47.04 146.36 514.62 1401.98 2110.0
15384 Zimbabwe ZWE 2014 59.360001 NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
15385 Zimbabwe ZWE 2015 60.397999 NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

5 rows × 21 columns

Erklärung zur Interpolation

Fehlende Datenwerte zwischen den Jahren 2013 und 2015 können mit verschiedenen Techniken ergänzt werden. Eine Möglichkeit ist diese mit der Funktion interpolate() des Pandas Series Objektes linear zu interpolieren. Als Startpunkt wurde hier eine einfache, lineare Interpolation gewählt.

In [302]:
# Convert column 'Year' to datetime for time-indexing
dfToPlot.loc['Year'] = pd.to_datetime(dfToPlot['Year'], format='%Y', infer_datetime_format=True).dt.year
# Indexing with time-series data to create datetime index
dfToPlot = dfToPlot.set_index(['Year'])
# Summary of the dataset
dfToPlot.isnull().sum()
Out[302]:
Entity                                  1
Code                                    1
LifeExpectancy                          1
AnnualFruitConsumptionPerCapita        17
AnnualVegetableConsumptionPerCapita    17
KcalOther                              17
KcalSugar                              17
KcalOilsFats                           17
KcalMeat                               17
KcalDairyEggs                          17
KcalFruitsVegetables                   17
KcalStarchyRoots                       17
KcalPulses                             17
KcalCerealsGrains                      17
KcalAlcoholicBeverages                 17
KcalAnimalProtein                      17
KcalPlantProtein                       17
KcalFat                                17
KcalCarbohydrates                      17
DailyCaloriesPerCapita                 17
dtype: int64
In [303]:
dfToPlot.tail()
Out[303]:
Entity Code LifeExpectancy AnnualFruitConsumptionPerCapita AnnualVegetableConsumptionPerCapita KcalOther KcalSugar KcalOilsFats KcalMeat KcalDairyEggs KcalFruitsVegetables KcalStarchyRoots KcalPulses KcalCerealsGrains KcalAlcoholicBeverages KcalAnimalProtein KcalPlantProtein KcalFat KcalCarbohydrates DailyCaloriesPerCapita
Year
2012.0 United States USA 78.820999 103.37 118.16 27.0 597.0 899.0 457.0 426.0 186.0 99.0 36.0 799.0 161.0 279.04 159.68 1460.07 1788.21 3687.0
2013.0 United States USA 78.959999 104.53 113.96 30.0 600.0 890.0 459.0 424.0 188.0 92.0 39.0 801.0 159.0 279.12 159.28 1453.86 1789.74 3682.0
2014.0 United States USA 79.099998 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2015.0 United States USA 79.244003 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
In [304]:
# 1. Aufgabe DailyCaloriesPerCapita Plot
ax = plt.gca()
for country in countries:
    y = dfToPlot.loc[dfToPlot['Entity'] == country]
    y = y['KcalFat']
    y.plot(kind='line', xticks=y.index, figsize=(45, 20), grid=True, ax=ax)
    
plt.legend(countries, fontsize=35, bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.1)
plt.xlabel('Year', fontsize=35)
plt.ylabel('Fat Consumption in Kcal', fontsize=35)
plt.title('Time Series Analysis, Fat Consumption per Year', fontsize=50)
plt.show()

# Store png
plt.savefig('../data/png/time_series_fatcons.png')
<Figure size 432x288 with 0 Axes>
In [305]:
# 2.Aufgabe
ax = plt.gca()
for country in countries:
    y = dfToPlot.loc[dfToPlot['Entity'] == country]
    y = y['DailyCaloriesPerCapita']
    y.plot(xticks=y.index, figsize=(40,20), grid=True, ax=ax)
plt.legend(countries, fontsize=25, bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.1)
plt.xlabel('Year', fontsize=25)
plt.ylabel('Fat Consumption in Kcal', fontsize=25)
plt.title('Time Series Analysis, Daily Calories per Capita and Year', fontsize=30)
plt.show()

# Store png
plt.savefig('../data/png/time_series_caloriespercap.png')
<Figure size 432x288 with 0 Axes>

Interpretation der graphischen Darstellung

Die zu untersuchende, erste Grafik 'Time Series Analysis, Annual Fat Consumption (Kcal) ', als auch die zweite Grafik 'Time Series Analysis, daily Calories per Capita' zeigen die Entwicklung des Kalorien- per Capita, als auch Fett Konsums, innerhalb verschiedener Länder, in Abhängigkeit der Zeit. Zur Untersuchung verschiedener Gesetzmäßigkeiten werden die Werte von acht, global verteilten Ländern herangezogen. Diese umfassen fünf Schwellenländer, die derzeit noch zu den globalen Entwicklungsländern zählen, sowie drei Industrie Nationen. Die Entwicklungsländer liegen im südamerikanischen Raum Bolivien, als auch Brasilien, sowie im Wirtschaftsraum EMEA mit Bulgarien und Ägypten. Dazu kommt Zentralafrika mit Kenya. Dem gegenüber stehen drei Industrienationen, sowohl im asiatischen Raum mit China, als auch zwei westlich Nationen aus Zentral Europa, sowie Nord Amerika mit Deutschland, sowie den Vereinigten Amerikanischen Staaten. Die dargestellten Werte haben einen jährlich wiederkehrenden Messzeitpunkt und verteilen sich über einen Zeitabschnitt von 20 Jahren zwischen den Jahren 1995 und 2015.

Bei der Analyse beider Grafiken ist insgesamt ein kontinuierlicher Aufwärtstrend erkennbar. Die Zeitreihen weisen langfristige Veränderungen in ihrem Niveau auf und sind damit nichtstationär. Gleichzeitig unterliegen die Zeitreihen innerhalb des Zeitraumes keinen saisonellen Entwicklungen. Sich wiederholende Entwicklungen innerhalb eines Jahres können in den vorliegenden Zeitreihendaten jedoch nicht ausgemacht werden, da für die Analyse (z.B. bei saisonellen Schwankungen zwischen Sommer und Wintermonaten) die einzelnen Zeitangaben fehlen. Auch starke Ausreißer innerhalb der Zeitreihe, sowie zyklische Enwicklungen sind nicht erkennbar.

In der ersten Grafik ist ein deutlicher Niveau Unterschied zwischen den verschiedenen Ländern erkennbar. So befinden sich die Industrienationen USA mit einem Mittelwert von 1403 Kcal/Jahr, sowie Deutschland mit einem Mittelwert von 1276 Kcal/Jahr am oberen Ende der Skala, während Bolivien mit einem Mittelwert von 426 Kcal/Jahr, neben Kenya mit 431 Kcal/Jahr, den geringsten Fett Konsum verzeichnet. Dies korreliert mit den Ergebnissen des pro Kopf Kalorienverbrauchs der zweiten Grafik. Die USA hat auch hier, dicht gefolgt von Deutschland mit 3415 Kalorien/Tag, den größten pro Kopf Verbrauch mit täglich 3701 Kalorien. Zum Vergleich benötigt ein Mann zwischen 25 und 51 Jahre durchschnittlich 2.400 Kalorien am Tag. Dies lässt darauf schließen, dass sowohl die deutsche, als auch die nordamerikanische Bevölkerung vermehrt an Adipositas, sowie Folgekrankheiten leidet.

Auffällig ist bei Betrachtung von Unregelmäßigkeiten, dass seit dem Jahr 2005 eine negative Trendwende in den Industriestaaten, vor allem den USA statt gefunden hat. Während der Fettverbrauch pro Jahr nur leicht zurück ging, wurde der Kalorienverbrauch pro Kopf deutlich rückläufig. Bei nahezu allen Entwicklungsländern ging dagegen der Verbrauch insgesamt mit einem positiven Trend nach oben und man kann hier davon ausgehen, in Abhängigkeit der wirtschaftlichen Lage des jeweiligen Landes, dass sich dieser Aufwärtstrend auch in Zukunft weiter positiv entwickeln wird.

Jahr mit den meisten non-Nans

  1. Bestimmen Sie das Jahr, für das am meisten non-Nans in dem in der vorigen Teilaufgabe erstellten Dataframe nutrition_information existieren.
  2. Erstellen Sie einen neuen Dataframe, der nur die non-Nan Daten für das zuvor bestimmte Jahr enthält.
  3. Mergen Sie diesen neuen Dataframe mit dem Dataframe general_information ohne die Spalte AnnualWorkingHourPerPerson. Der so gebildete Dataframe wird im Weiteren mit data_nut_gen bezeichnet.
In [237]:
# 1. Aufgabe

# Neues DataFrame das alle Jahre und die Anzahl der non-Nans in dieser Spalte enthält
df = pd.DataFrame()
df['Year'] = nutrition_information['Year']
df['non-Nan'] = nutrition_information.count(axis=1)   

# Dictionary in dem zu jedem Jahr die Anzahl an Non-Nan Spalten zugewiesen wird
Non_nans_count = {} 
non_nans_count = []

# Maximale Anzahl an Spalten die nicht Null sind
maximal = df["non-Nan"].max()

for label, row in df.iterrows():
    # Wenn die Anzahl an Non-Nan Spalten dem Maximum entspricht, wird es der Liste 'non_nans_count' angefügt
    if row[1] == maximal:
        non_nans_count.append(row[0])

# Für jedes Jahr wird gezählt wie viele Non-Nan Spalten in der Liste 'non_nans_count' enthalten sind 
for i in nutrition_information['Year'].unique():
    # Für jedes Jahr (=Key) wird die Anzahl (=Value) dem Dictionary 'Non_nans_count' zugewiesen
    Non_nans_count[i] = non_nans_count.count(i)
    
[k for k,v in Non_nans_count.items() if v == max(Non_nans_count.values())]
Out[237]:
[2012, 2013]
In [239]:
# 2. Aufgabe
# Entscheidung fiel für das Jahr 2013

# Datenbankabfrage um alle Daten von der table 'nutrition_information' vom Jahr 2013 zu selektieren
query = """SELECT * FROM nutrition_information WHERE "Year"= '2013'"""
df_2013 = pd.read_sql_query(query,engine)
# Drop all NaN values
df_2013 = df_2013.dropna()

# Datenbankabfrage um alle Daten außer 'AnnualWorkingHourPerPerson' von der table 'general_information' vom Jahr 2013 zu selektieren
query = """SELECT "Entity", "Code", "Year","LifeExpectancy","GDPperCapita","AnnualHealthcarExpPerCapita" FROM general_information WHERE "Year"= '2013'"""
general_information_2013 = pd.read_sql_query(query,engine)
In [241]:
# 3. Aufgabe

# Merge vom DataFrame df_2013  und general_information_2013 
data_nut_gen = general_information_2013.merge(df_2013, how="left", on=["Entity","Code","Year","LifeExpectancy"])
# Löschen von noch vorhandenen NaN Spalten
data_nut_gen = data_nut_gen.dropna()
# Resetet Index weil diese nach dem drop lückenhaft sind
data_nut_gen.index = range(len(data_nut_gen))
# Show first values
data_nut_gen.head(5)
Out[241]:
Entity Code Year LifeExpectancy GDPperCapita AnnualHealthcarExpPerCapita AnnualFruitConsumptionPerCapita AnnualVegetableConsumptionPerCapita KcalOther KcalSugar ... KcalFruitsVegetables KcalStarchyRoots KcalPulses KcalCerealsGrains KcalAlcoholicBeverages KcalAnimalProtein KcalPlantProtein KcalFat KcalCarbohydrates DailyCaloriesPerCapita
0 Afghanistan AFG 2013 62.493999 1814.155825 156.529349 34.05 28.97 15.0 87.0 ... 68.0 15.0 23.0 1560.0 0.0 48.88 184.12 301.68 1555.32 2090.0
1 Albania ALB 2013 77.702003 10504.093089 564.065965 142.00 245.09 33.0 197.0 ... 372.0 78.0 50.0 1144.0 73.0 237.68 208.00 958.77 1788.55 3193.0
2 Algeria DZA 2013 75.417999 13253.217515 858.860356 114.19 165.70 36.0 265.0 ... 339.0 126.0 79.0 1678.0 7.0 99.96 267.88 688.95 2239.21 3296.0
3 Angola AGO 2013 60.373001 6185.013829 301.989149 90.34 67.66 13.0 146.0 ... 166.0 729.0 81.0 717.0 115.0 73.60 155.44 481.86 1762.10 2473.0
4 Antigua and Barbuda ATG 2013 75.891998 18862.816760 1121.228751 157.78 74.32 62.0 302.0 ... 228.0 45.0 28.0 651.0 84.0 227.32 107.52 810.27 1271.89 2417.0

5 rows × 23 columns

Dimensionsreduktion mit PCA und TSNE

Der oben konstruierte Dataframe data_nut_gen sollte ausschließlich non-NAN Werte haben und Daten nur eines Jahres enthalten. Alle Spalten von data_nut_gen, außer Entity, Code, Year und LifeExpectancy werden im Folgenden als Merkmalsspalten bezeichnet.

  1. Führen Sie auf dem numpy-Array, das nur die Werte der Merkmalspalten enthält, eine Principal Component Analysis mit dem entsprechenden scikit-learn Modul PCA durch. Bestimmen Sie die zwei Hauptachsen.
  2. Bestimmen Sie für das trainierte Modell den explained_variance_ratio. Was sagen diese Zahlen aus?
  3. Stellen Sie die auf die 2 Hauptachsen transformierten Daten der Merkmalsspalten von data_nut_gen in einem Bokeh-Plot dar. In diesem soll die Farbe der Punkte durch die Werte der Spalte LifeExpectancy codiert werden.
  4. Führen Sie nun eine Dimensionalitätsreduktion mit dem scikit-learn Modul TSNE auf 2 Achsen durch. Visualisieren Sie auch dieses Resultat mit einem Bokeh-Plot (wie in Teilaufgabe 2).
  5. Diskutieren Sie das Ergebnis der beiden Dimensionsreduktionen.
In [243]:
from sklearn.decomposition import PCA
In [245]:
# 1. Aufgabe
import numpy as np
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

features = ['GDPperCapita', 'AnnualHealthcarExpPerCapita', 'AnnualFruitConsumptionPerCapita', 'AnnualVegetableConsumptionPerCapita',
            'KcalOther', 'KcalSugar', 'KcalOilsFats']
X = data_nut_gen.loc[:, features].values

scaler = StandardScaler()
scaler.fit(X)
X = scaler.transform(X)

pca = PCA(n_components=2)
pca.fit(X)

X_pca = pca.transform(X)

print("original shape: ", X.shape)
print("transformed shape: ", X_pca.shape)

Df = pd.DataFrame(data = X_pca, columns = ['principal_component_1', 'principal_component_2'])

finalDf = pd.concat([Df,data_nut_gen['LifeExpectancy']], axis = 1)
original shape:  (160, 7)
transformed shape:  (160, 2)
In [246]:
# 2. Aufgabe

# explained_variance_ratio_ beider Hauptkomponenten
print('explained_variance_ratio_: ', pca.explained_variance_ratio_)
# explained_variance_ratio_ von Hauptkomponente 1
print('Hauptkomponente 1 explained_variance_ratio_: ', pca.explained_variance_ratio_[0])
# explained_variance_ratio_ von Hauptkomponente 2
print('Hauptkomponente 2 explained_variance_ratio_: ', pca.explained_variance_ratio_[1])
# Summe beider Komponenten
print('Summe beider Hauptkomponenten: ', pca.explained_variance_ratio_[0]+pca.explained_variance_ratio_[1])
explained_variance_ratio_:  [0.48 0.15]
Hauptkomponente 1 explained_variance_ratio_:  0.4827952522565093
Hauptkomponente 2 explained_variance_ratio_:  0.15090487913237396
Summe beider Hauptkomponenten:  0.6337001313888833

Was sagen diese Zahlen aus?

'explained variance ratio' gibt an, wie viel Information (variance) den einzelnen Hauptkomponenten zugeordnet werden kann. Diese ist wichtig, da man z.B. einen 7-dimensionalen Raum in einen 2-dimensionalen Raum umwandeln kann, dabei aber etwas von der Varianz verliert. In unserem Beispiel hat die erste Hauptkomponente eine Varianz von ~48,28% und die zweite Hauptkomponente eine Varianz von ~15,09%. Daraus ergibt sich, dass beide Komponenten zusammen noch ~63,37% der Information enthalten.

In [247]:
# 3. Aufgabe

#Color_mapper und color_bar zum farblichen codieren der Lebenserwartung
color_mapper = LinearColorMapper(palette=Viridis256, low=finalDf.LifeExpectancy.min(), high=finalDf.LifeExpectancy.max())

color_bar = ColorBar(color_mapper=color_mapper, 
                     label_standoff=12, border_line_color=None, location=(0,0))
TOOLTIPS = [
    ('Lebenserwartung', '@LifeExpectancy')
    ]

#Scatterplot erstellen
p = figure(plot_height = 500, plot_width= 500, background_fill_color="lightgrey",tooltips=TOOLTIPS)
p.scatter('principal_component_1', 'principal_component_2', source=finalDf, color={'field': 'LifeExpectancy', 'transform': color_mapper})
p.add_layout(color_bar, 'right')

show(p)
In [248]:
from sklearn.manifold import TSNE
from sklearn.preprocessing import StandardScaler
In [249]:
# 4. Aufgabe

#Daten filter und numpy array in numpy array umwandeln
data_nut_gen_tsne = data_nut_gen.reset_index()[data_nut_gen.columns.difference(['Entity', 'Code', 'Year', 'LifeExpectancy'])]
data_nut_gen_tsne = data_nut_gen_tsne.to_numpy()

#Daten standardisieren
scaler = StandardScaler()
scaler.fit(data_nut_gen_tsne)
data_nut_gen_tsne = scaler.transform(data_nut_gen_tsne)

#TSNE durchführen
data_nut_gen_tsne = TSNE(n_components=2, perplexity=10).fit_transform(data_nut_gen_tsne)
#Ergebnis in DataFrame wandeln
data_nut_gen_tsne = pd.DataFrame(data = data_nut_gen_tsne, columns = ['tsne_1', 'tsne_2'])
#Spalte mit Lebenserwartung anhängen
data_nut_gen_tsne = pd.concat([data_nut_gen_tsne, data_nut_gen.reset_index()[['LifeExpectancy']]], axis = 1)

#Color mapper/bar für farbliche Visualisierung der Lebenserwartung
color_mapper = LinearColorMapper(palette=Viridis256, low=data_nut_gen_tsne.LifeExpectancy.min(), high=data_nut_gen_tsne.LifeExpectancy.max())

color_bar = ColorBar(color_mapper=color_mapper, 
                     label_standoff=12, border_line_color=None, location=(0,0))

#Bokeh Plot erstellen
p = figure(plot_height = 600, plot_width= 600, background_fill_color="lightgrey")
p.scatter('tsne_1', 'tsne_2', source=data_nut_gen_tsne, color={'field': 'LifeExpectancy', 'transform': color_mapper},radius=0.5)
p.add_layout(color_bar, 'right')
show(p)

5. Aufgabe

Bei beiden Dimensionsreduktionsverfahren lässt sich ein Verlauf der Lebenserwartung erkennen. Bei der PCA sind die einzelnen Punkte in einer "Wolke" angeordnet. Beim TSNE sind die Punkte weiter verteilt und ähnliche Lebenserwartungen sind näher beieinander. Die Länder sind klarer gruppiert. Cluster sind besser zu erkennen.

Regression

Im Folgenden soll aus den Merkmalsspalten das Dataframes data_nut_gen die Lebenserwartung (Spalte LifeExpectancy) vorhergesagt werden.

  1. Partitionieren Sie die Daten mit der scikit-learn Methode, so dass $2/3$ der Daten für das Training und der Rest für den Test verwendet werden.
  2. Trainieren Sie ein lineares Regressionsmodell mit den Trainingsdaten und bestimmen Sie den Mean-Absolute-Error und den R2-Score auf Trainings- und Testdaten.
  3. Wie kann mit dem trainierten Linear-Regression Modell auf die Bedeutung (Wichtigkeit) der verwendeten Merkmale geschlossen werden.
  4. Trainieren Sie nun einen Random Forest Regressor und bestimmen Sie wieder Mean-Absolute-Error und den R2-Score auf Trainings- und Testdaten.
  5. Wie kann mit dem trainierten Random-Forest Modell auf die Bedeutung (Wichtigkeit) der verwendeten Merkmale geschlossen werden.
  6. Was ist der R2-Score?
  7. Diskutieren Sie die Ergebnisse
In [250]:
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
In [251]:
# Investigation of data quality
data_nut_gen[data_nut_gen.isnull().any(axis=1)][:5]
Out[251]:
Entity Code Year LifeExpectancy GDPperCapita AnnualHealthcarExpPerCapita AnnualFruitConsumptionPerCapita AnnualVegetableConsumptionPerCapita KcalOther KcalSugar ... KcalFruitsVegetables KcalStarchyRoots KcalPulses KcalCerealsGrains KcalAlcoholicBeverages KcalAnimalProtein KcalPlantProtein KcalFat KcalCarbohydrates DailyCaloriesPerCapita

0 rows × 23 columns

In [252]:
# 1. Aufgabe - Test train split data for supervised learning with Multiple Linear Regression

# Saving feature names for later use
feature_list = list(data_nut_gen.columns)
# X= multiple input variables, y = output variable 'LifeExpectancy'
X, y = data_nut_gen.iloc[:, 4:12], data_nut_gen.iloc[:, 3]
# Split training and test data sets 
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42) # set to 42 means the results will be the same each time the split is run
# Split val and test data
y_val = train_test_split(y, shuffle=True)
In [253]:
# Check for dimensionalilty of split data
print('Training Features Shape:', X_train.shape)
print('Training Labels Shape:', y_train.shape)
print('Testing Features Shape:', X_test.shape)
print('Testing Labels Shape:', y_test.shape)
Training Features Shape: (107, 8)
Training Labels Shape: (107,)
Testing Features Shape: (53, 8)
Testing Labels Shape: (53,)
In [254]:
# 2. Aufgabe 

# Define the multiple Linear Regression model and train it
lr_model = LinearRegression().fit(X_train, y_train)
# Predict the target values for the given features
y_pred = lr_model.predict(X_test)
# Plot prediction Array
print ('Predicted Life Expectancy per Country in Years: \n', y_pred)
Predicted Life Expectancy per Country in Years: 
 [84.47 61.94 61.33 71.96 70.5  76.6  66.84 66.96 60.   70.85 65.9  76.89
 71.46 73.95 63.46 77.74 73.26 68.51 79.15 61.24 62.51 69.89 76.7  68.62
 74.31 92.73 67.97 68.67 70.62 83.36 72.47 68.3  62.11 67.81 69.88 75.19
 57.89 71.53 78.65 75.81 82.55 82.22 60.99 67.09 74.64 73.93 75.17 69.52
 60.14 65.57 66.13 58.97 63.29]
In [255]:
# Calculate the absolute errors
errors = abs(y_pred - y_test)
# Calculate mean absolute percentage error (MAPE)
mape = 100 * (errors / y_test)
# Calculate and display accuracy
accuracy = 100 - np.mean(mape)
print('Mean Absolute Percentage Error:', round(mape.mean(), 2), '%.')
print('Accuracy:', round(accuracy, 2), '%.')
Mean Absolute Percentage Error: 5.04 %.
Accuracy: 94.96 %.
In [256]:
# The mean squared error
print('Mean squared error: %.2f'
      %  mean_squared_error(y_test, y_pred))
# The mean absolute error
print('Mean absolute error: %.2f'
      %  mean_absolute_error(y_test, y_pred))
# The coefficient of determination: 1 is perfect prediction
print('Coefficient of determination: %.2f'
      % r2_score(y_test, y_pred))
Mean squared error: 21.75
Mean absolute error: 3.48
Coefficient of determination: 0.58
In [257]:
# 3. Aufgabe 

# The coefficients
coeff_df = pd.DataFrame(lr_model.coef_, X.columns, columns=['Coefficient'])
print(coeff_df)
Coefficient
GDPperCapita                           -0.000008
AnnualHealthcarExpPerCapita             0.001492
AnnualFruitConsumptionPerCapita         0.012942
AnnualVegetableConsumptionPerCapita     0.028848
KcalOther                              -0.002532
KcalSugar                               0.017642
KcalOilsFats                            0.002382
KcalMeat                                0.015657

Bedeutung (Wichtigkeit) der verwendeten Merkmale

Die Merkmale, welche einen hohen Einfluss auf das Ergebnis y des Models haben können mittels deren Koeffizienten dargestellt und verglichen werden. In der dargestellten Tabelle lässt sich ablesen, dass bei der Erhöhung eines Merkmals um eine Einheit sich der Output um den entsprechenden Wert erhöht, bzw. verringert. Bei der Erhöhung des Merkmals x8 KcalOilFat um eine Einheit würde sich daher die Lebenserwartung um 0.001017 verringern.

Insgesamt haben die Merkmale x5 AnnualVegetableConsumptionPerCapita,x7 KcalSugar, sowie x4 AnnualFruitConsumptionPerCapita die größte Auswirkung und damit die höchste Bedeutung für den Output y.

In [258]:
# Random Forest Regression model
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import cross_val_score, GridSearchCV
In [259]:
# 4. Aufgabe

# Instantiate model with 1000 decision trees
rf_model = RandomForestRegressor(n_estimators=1000, max_depth=7, random_state=42)
# Train the model on training data
rf_model.fit(X_train, y_train)
Out[259]:
RandomForestRegressor(bootstrap=True, ccp_alpha=0.0, criterion='mse',
                      max_depth=7, max_features='auto', max_leaf_nodes=None,
                      max_samples=None, min_impurity_decrease=0.0,
                      min_impurity_split=None, min_samples_leaf=1,
                      min_samples_split=2, min_weight_fraction_leaf=0.0,
                      n_estimators=1000, n_jobs=None, oob_score=False,
                      random_state=42, verbose=0, warm_start=False)
In [260]:
# Use the forest's predict method on the test data
y_rf_pred = rf_model.predict(X_test)
# Calculate the absolute errors
errors = abs(y_rf_pred - y_test)
# Print out the mean absolute error (mae)
print('Mean Absolute Error:', round(np.mean(errors), 2), 'years.')
Mean Absolute Error: 2.95 years.
In [261]:
# The mean squared error
print('Mean squared error: %.2f'
      %  mean_squared_error(y_test, y_rf_pred))
# The mean absolute error
print('Mean absolute error: %.2f'
      %  mean_absolute_error(y_test, y_rf_pred))
# The coefficient of determination: 1 is perfect prediction
print('Coefficient of determination: %.2f'
      % r2_score(y_test, y_rf_pred))
Mean squared error: 16.72
Mean absolute error: 2.95
Coefficient of determination: 0.68
In [262]:
# Calculate mean absolute percentage error (MAPE)
mape = 100 * (errors / y_test)
# Calculate and display accuracy
accuracy = 100 - np.mean(mape)
print('Accuracy:', round(accuracy, 2), '%.')
Accuracy: 95.62 %.
In [288]:
feature_list
Out[288]:
['Entity',
 'Code',
 'Year',
 'LifeExpectancy',
 'GDPperCapita',
 'AnnualHealthcarExpPerCapita',
 'AnnualFruitConsumptionPerCapita',
 'AnnualVegetableConsumptionPerCapita',
 'KcalOther',
 'KcalSugar',
 'KcalOilsFats',
 'KcalMeat',
 'KcalDairyEggs',
 'KcalFruitsVegetables',
 'KcalStarchyRoots',
 'KcalPulses',
 'KcalCerealsGrains',
 'KcalAlcoholicBeverages',
 'KcalAnimalProtein',
 'KcalPlantProtein',
 'KcalFat',
 'KcalCarbohydrates',
 'DailyCaloriesPerCapita']
In [264]:
# 5. Aufgabe

# Check for relative importance of the feature variables
# Get numerical feature importances
importances = list(rf_model.feature_importances_)
# List of tuples with variable and importance
feature_importances = [(feature, round(importance, 2)) for feature, importance in zip(feature_list[3:], importances)]
# Sort the feature importances by most important first
feature_importances = sorted(feature_importances, key = lambda x: x[1], reverse = True)
# Print out the feature and importances 
[print('Variable: {:20} Importance: {}'.format(*pair)) for pair in feature_importances]
Variable: Code                 Importance: 0.54
Variable: Entity               Importance: 0.17
Variable: AnnualVegetableConsumptionPerCapita Importance: 0.14
Variable: AnnualFruitConsumptionPerCapita Importance: 0.05
Variable: AnnualHealthcarExpPerCapita Importance: 0.04
Variable: LifeExpectancy       Importance: 0.03
Variable: GDPperCapita         Importance: 0.03
Variable: Year                 Importance: 0.01
Out[264]:
[None, None, None, None, None, None, None, None]
In [265]:
# Set the style
plt.style.use('fivethirtyeight')
# list of x locations for plotting
x_values = list(range(len(importances)))
# Make a bar chart
plt.bar(x_values, importances, orientation = 'vertical')
# Tick labels for x axis
plt.xticks(x_values, feature_list, rotation='vertical')
# Axis labels and title
plt.ylabel('Importance') 
plt.xlabel('Variable') 
plt.title('Variable Importances')
Out[265]:
Text(0.5, 1.0, 'Variable Importances')

Bedeutung (Wichtigkeit) der verwendeten Merkmale

Untersuchung der Bedeutung von Merkmalsvariablen für die Performance des Models. Die mittels Skicit-Learn zurückgegebenen Werte der Bedeutsamkeit je Merkmal stellen dar, wie sehr die Einbeziehung einer bestimmten Variable die Vorhersage des Random Forest Regression Algorithmus verbessert. Mittels den tabellarisch aufgelisteten Werten kann ein Vergleich zwischen den Merkmalsvariablen angestrebt werden.

Ganz oben steht dabei das Merkmal AnnualHealthcareExpPerCapita. Dies sagt aus, dass der beste Prädikator für die Lebenserwartung die jährlichen Ausgaben in das Gesundheitssystem eines Staates pro Kopf sind. Das zweit wichtigste Merkmal wird unter GDPperCapita gelistet. Je höher das Bruttoinlandsprodukt (BPI) eines Landes ist desto höher der Wohlstand der Einzelpersonen und desto höher die Lebenserwartungen innerhalb einer Industrienation. Einen ebenso hohen Einfluss hat der Konsum von Fleisch, Ölen und Fetten auf die Lebenserwartung.

Dagegen haben der jährliche pro Kopf Frucht und Gemüse Konsum die geringste Bedeutung für die korrekte Vorhersage der jährlichen Lebenserwartung der Bevölkerung eines Landes. Bei zukünftigen Implementierungen des Modells können diese Variablen daher auch entfernt werden, ohne die Leistung des Algorithmus einzuschränken.

Was ist der R² ( R-Squared ) Score?

Der R²-Score ist ein Bestimmungsmaß zu Festlegung der Güte eines trainierten Linear Regression Models. Eine abhängige Variable $y_{i}$ lässt sich als die Summe aus dem vorhergesagten Wert $^y_{i}$ der Abweichung ( Varianz ) zwischen einem beobachteten und dem vom Modell vorhergesagten Wert $e_{i}$ ( Residuum ) berechnen. Je nach verwendeter Anzahl unabhängiger Variablen $x_{i}$ handelt es sich dabei um eine einfache Regression ( nur eine unabhängige Variable ) oder um eine multiple Regression ( mehrere unabhängige Variablen ).

Wichtig ist dabei zu verstehen, wie gut die unabhängigen Variablen dafür geeignet sind, um die Varianz der abhängigen Variabel $y_{i}$ zu erklären, bzw. neue Werte hervorzusagen. Hierfür ist der R²-Score , welcher einen Wert zwischen 0 und 1 erreichen kann geeignet. Je näher der Wert des R²-Score am Wert 0 liegt, desto ungeeigneter sind die gewählten Variablen, um $y_{i}$ vorherzusagen. Dies kann auch als poor model fit bezeichnet werden. Auf der anderen Seite kann man sagen, je näher der Wert an 1 liegt, desto näher ist die Regressionsgerade an den Eingabewerten.

Diskussion der Ergebnisse

Das Linear Regression Model konnte eine Accuracy von 93.54%, mit einem Mean Absolute Error (MAE) von 4.46% und einem R²-Score von 45% erreichen, während das Random Forest Regression Modell unter Training mit den selben unabhängigen Variablen $x_{i}$ deutlich bessere Werte, mit einer Accuracy von 95.1%, mit einem Mean Absolute Error (MAE) von 3.23% und R²-Score von 65% erreichen konnte.

Auch die Wichtigkeit der einzelnen, verwendeten Merkmale unterscheidet sich zwischen dem Linear Regression Model (LRM) und dem Random Forest Regression Model (RFRM). So hat beim LRM das Merkmal AnnualVegetableConsumptionPerCapita den größten, positiven Regressions Koeffizienten, d.h. vergrößert sich der Wert von xi um eine Einheit, so würde sich auch der Mittelwert der abhängigen Variable yi vergrößern. Ein negativer Wert zeigt jedoch auf, dass hier das Gegenteil der Fall wäre. Dies würde suggerieren, dass es für das Training des Models Sinn macht, den statistisch nicht signifikaten Variable $x_{i}$ aus den verwendeten Merkmalsvariablen zu entfernen, da diese die Präzision des Ergebnisses sogar verringern kann. Auch der R² Wert fällt relativ gering aus, was darauf hinweisen kann, dass die verwendeten unabhängigen Variblen statistisch nicht signifikant sind für die Varianz der abhängigen Variablen $y_{i}$. Beim RFRM Model hat dagegen die Merkmalsvariable AnnualHealthcareExpPerCapita die stärkste Relevanz als Prädikator für die abhängige Variable $y_{i}$.

Bei einer visuellen Analyse auf Linearität der Trainingsdaten konnte bei den unabhängigen Variablen AnnualHealthcareExpPerCapita als auch GDPperCapita eine geringere lineare Abhängigkeit aufgezeigt werden. Das LRM ist jedoch ein lineares Modell, welches gut geeignet ist für Daten mit einer linearen Form. Dies könnte eine Erklärung sein für das schlechtere Abschneiden einzelner Datensätze beim R²-Score sein, im Verhältnis zum RFRM. Auf der anderen Seite hat ein RFRM Schwierigkeiten die Linearkombinationen einer großen Anzahl von Merkmalen zu modellieren.

Clustering der Länder nach Ihrem Pro-Kopf-Kalorienverbrauch

  1. Bestimmen Sie zunächst alle Länder für die das Merkmal DailyCaloriesPerCapita im Dataframe nutrition_information für alle Jahre von 1960 bis 2012 einen Wert hat. Konstruieren Sie dann ein numpy-Array $X$, in dem das Element $C_{i,j}$ in Zeile $i$, Spalte $j$, den DailyCaloriesPerCapita-Wert des Landes $i$ im $j.ten$ Jahr zwischen 1960 und 2012 enthält.
  2. Clustern Sie die Länder nach ihrer zeitlichen Entwicklung des DailyCaloriesPerCapita-Wertes - also die Zeilen des Arrays $X$. Verwenden Sie hierfür den K-Means Algorithmus von scikit-learn. Stellen Sie dabei die Clusteranzahl auf $k=5$ ein.
  3. Für jedes der $k=5$ Cluster: Erzeugen Sie einen Plot, in dem der zeitliche Verlauf des DailyCaloriesPerCapita-Wertes aller zum jeweiligen Cluster gehörenden Länder als Line-Graph dargestellt ist.
  4. Charakterisieren Sie die 5 Cluster anhand der clusterspezifischen Visualisierung.
In [266]:
from sklearn.cluster import KMeans
In [267]:
# 1. Aufgabe

X = nutrition_information.copy()
X = X.reset_index()
X = X[["Entity", "Year", "DailyCaloriesPerCapita"]]
X = X.set_index("Entity")
X = X.loc[X["Year"].isin(range(1961,2013))] #ab 1961 da für das Jahr 1960 nur China einen Wert hat
#Tabelle in passende Form bringen
X = X.pivot(index=X.index, columns='Year')['DailyCaloriesPerCapita']
X = X.dropna()
Xnumpy = X.to_numpy()
Xnumpy
Out[267]:
array([[2999., 2917., 2698., ..., 2104., 2107., 2100.],
       [2223., 2242., 2156., ..., 3076., 3132., 3184.],
       [1619., 1569., 1528., ..., 3142., 3217., 3272.],
       ...,
       [1792., 1800., 1819., ..., 2197., 2206., 2231.],
       [2155., 2152., 2105., ..., 1904., 1907., 1923.],
       [2115., 2161., 2148., ..., 2168., 2200., 2197.]])
In [268]:
# 2. Aufgabe

# KMeans anwenden
kmeans = KMeans(n_clusters=5, random_state=0).fit(X)
# Cluster zuordnen
X["Cluster"] = kmeans.labels_
In [269]:
# 3. Aufgabe

# Cluster plotten
for i in range(5):
    toPlot = X.loc[X['Cluster'] == i]
    toPlot = toPlot.drop(['Cluster'], axis=1)
    toPlot.T.plot(figsize=(20,10), title="Cluster " + str(i))

Aufgabe 4:

Der k-Means-Algorithmus ist ein Rechenverfahren, das sich für die Gruppierung von Objekten, die sogenannte Clusteranalyse, einsetzen lässt. Dank der effizienten Berechnung der Clusterzentren und dem geringen Speicherbedarf eignet sich der Algorithmus sehr gut für die Analyse großer Datenmengen, wie sie im Big-Data-Umfeld üblich sind. Der Algorithmus ist in der Lage, aus einer Menge ähnlicher Objekte mit einer vorher bekannten Anzahl von Gruppen die jeweiligen Zentren der Cluster zu ermitteln. Da es sich um ein sehr effizientes Verfahren handelt, das mit vielen verschiedenen Datentypen zurecht kommt, und der Speicherbedarf gering ist, eignet sich der k-Means-Algorithmus für die Datenanalyse im Big-Data-Umfeld.

Die Laufzeit des Algorithmus ist linear zur Anzahl der vorhandenen Datenpunkte und die Anzahl der Schleifendurchläufe zur Ermittlung der Clusterzentren ist klein.

Jede Gruppe wird durch einen ihr zugehörigen Mittelpunkt repräsentiert. Das Ziel des k-Means-Clustering-Algorithmus ist das automatische Berechnen dieser k-Mittelpunkte (sog. Cluster-Mittelpunkte), welche die bekannten Daten am besten repräsentieren. In unserem Fall ist k = 5

Zu Beginn der Berechnung werden fünf zufällig gewählte Mittelpunkte gewählt. Anschließend wird jeder Messwert dem am nächsten liegenden Cluster-Mittelpunkt zugeordnet. Für jeden Mittelpunkt wird nun das geometrische Zentrum seiner ihm zugeordneten Messwerte ermittelt. Dorthin wird der Mittelpunkt verschoben. Die letzten beiden Schritte werden nun theoretisch so lange wiederholt, bis sich die Positionen der Mittelpunkte nicht mehr wesentlich verändern. Durch die Verschiebung kann sich auch die Zuordnung der Daten zu den Mittelpunkten ändern. Da nicht genau an jedem Mittelpunkt ein Datensatz vorhanden ist, wird der geometrisch jeweils am nächsten liegende Datensatz verwendet. Ziel ist es, dass die verschiedenen Objekte innerhalb einer Gruppe sich nach der erfolgten Einteilung möglichst ähnlich sind.

Der k-Means-Algorithmus findet Anwendung auf Daten oder Objekte in einem n-dimensionalen Raum. Das Verfahren verläuft in folgenden Schritten:

1. Wahl von k-Punkten als Anfangszentren der Berechnung

2. Zuordnung der Datenpunkte zu den verschiedenen Clustern auf Basis des Abstands zu den Zentren

3. Neuberechnung der Clusterzentren

4. Wiederholung ab Schritt 2 – bis sich die Lage der Zentren nicht mehr ändert

In den ersten Durchläufen des Algorithmus treten noch große Änderungen der Zentren auf. Mit zunehmenden Schleifendurchläufen werden die Veränderungen immer kleiner. Wesentlich für einen effizienten Ablauf des Algorithmus ist die Wahl der Anfangszentren.

In echten Fällen benutzt man häufig wesentlich mehr als zwei Daten-Dimensionen. Es ist nicht ungewöhnlich, dass ein einzelner Datensatz aus zehn und mehr Komponenten besteht – was die Visualisierung und Anschaulichkeit an dieser Stelle allerdings erschwert hätte.In unserem Beispiel war die Anzahl k der Mittelpunkte vorgegeben. In der Praxis ist die Anzahl nicht immer klar spezifiziert. Dieser Herausforderung begegnen wir mit geschicktem Ausprobieren, d. h. es werden verschiedene Werte für k systematisch getestet und die jeweilige Güte der Lösung bewertet.

Ein Problem bei der Anwendung des k-Means-Algorithmus stellt die Vorgabe der Anzahl der Cluster und die Wahl der Anfangszentren dar. Die gefundene Lösung ist stark von den gewählten Startpunkten abhängig. Zu Wahl der Startpunkte ist die Kenntnis einer gewissen Clusterstruktur notwendig, die aus Voranalysen der Daten stammen könnte. In vielen Fällen erfolgt der Durchlauf des Algorithmus mit unterschiedlichen Startwerten. Die verschiedenen Ergebnisse liefern Hinweise auf eine möglichst plausible Struktur der Cluster. Verwendet man eine ungeeignete Anzahl von Clusterzentren als Startwerte, können sich unter Umständen komplett andere Lösungen oder ungeeignete Clustereinteilungen ergeben.

Auch problematisch für den k-Means-Algorithmus sind Datenmengen, die sich überlappen oder in Teilen nahtlos ineinander übergehen. In diesen Fällen ist das k-Means-Verfahren nicht in der Lage, die verschiedenen Gruppen zuverlässig voneinander zu trennen. Daten mit hierarchischen Clusterstrukturen werden ebenfalls nicht unterstützt. Sind Ausreißer in der Datenmenge vorhanden, können diese das Ergebnis stark verfälschen, da k-Means keine Ausreißer erkennt und jedes Objekt einem Cluster zuordnet. In diesen Fällen ist vor der Auswertung mit dem k-Means-Algorithmus eine Bereinigung der Daten (Noisereduktion) durchzuführen.

  • Cluster 0: Hier befinden sich Länder mit dem anfangs insgesamt höchsten DailyCaloriesPerCapita Wert, welcher sich über die Jahre weiterhin leicht steigert. In diesem Cluster befinden sich die viele Erste Welt Länder.
  • Cluster 1: Hier befinden sich Länder mit einem anfangs niedrigen DailyCaloriesPerCapita Wert, welcher über die Jahre kaum steigt, bzw. erst etwas sinkt und Beginn der 2000er Jahre beginnt zu steigen. In diesem Cluster befinden sich Länder die im Vergleich zu vorherigen Cluster weniger entickelt sind.
  • Cluster 2: In diesem Cluster befinden sich meist östliche Staaten, welche bis 1991 Teil der UdSSR waren. Im Bereich 1990 kann man einen Einbruch er DailyCaloriesPerCapita erkennen, was durch die damalige politische Lage und das verringerte Bruttoinlandsprodukt infolge des Zusammenbruchs der Planwirtschaft erklären ließe. Ab dem Jahr 1990 ist ein leichter kontinuierlicher anstieg bei allen Ländern erkennbar. Manche Länder haben zeitweilig einen extremen anstieg, wie etwa Georgien, während andere Länder wie Moldavien sich langsam steigen über die Jahre entwickeln.
  • Cluster 3: Hier befinden sich Länder mit einem niedrigen mittlerem DailyCaloriesPerCapita Wert, welcher sich über die Jahre stark steigert. Auffalend sind in diesem Cluster besonders die ansteigenden Werte für Iraq während den Jahren 1980 -1990, die zeitgleich mit der Machtübernahme Husseins 1979 und den Kriegen von 1980-1991 einhergehen.
  • Cluster 4: Hier befinden sich Länder mit einem anfangs mittlerem DailyCaloriesPerCapita Wert, welcher sich über die Jahre moderat steigert. Auffallend ist hier der starke Abfall der Werte von Kuweit um die Jahre 1990. Politisch lässt dies einen Zusammenhand in Betrachtung der Auffäligkeit des letzten Clusters durch den Einmarsch der irakischen Armee in Kuweit vermuten.

Der k-Means-Algorithmus findet in vielen Bereichen Anwendung. Aufgrund seiner Effizienz und des geringen Speicher- und Rechenbedarfs eignet er sich für die Datenanalyse großer Datenmengen im Big-Data-Umfeld. In der Bildverarbeitung wird k-Means häufig zur Segmentierung der Bilddaten eingesetzt. Mit den Ergebnissen des Algorithmus ist beispielsweise die Trennung von Vorder- und Hintergrund oder das Erkennen von einzelnen Objekten möglich. Ein weiterer wichtiger Anwendungsbereich sind das Marketing und die Analyse des Kundenverhaltens. k-Means findet in vorliegenden Kundendaten verschiedene Gruppen von Kunden mit jeweils ähnlichem Kundenverhalten. Die von k-Means ermittelten homogenen Gruppen lassen sich klar voneinander trennen. Mithilfe spezifischer Marketingmaßnahmen sind die Gruppen effizienter und mit größerer Erfolgsaussicht ansprechbar.os.system('jupyter nbconvert --to html yourNotebook.ipynb')

In [270]:
import os 
os.system('jupyter nbconvert --to html Project1_Global_HealthData_App.ipynb')
Out[270]:
0